package ija.model;

import ija.model.basis.Desk;
import ija.model.basis.Figure;
import ija.model.basis.Position;
import ija.model.figures.CheckersFigure;
import ija.model.figures.Piece;
import ija.model.figures.Queen;
import ija.model.history.History;
import ija.model.history.HistoryMove;
import ija.model.players.Player;
import ija.model.types.FigureColor;
import ija.model.types.FigureDirection;
import ija.xls.notation.DeskCoordinate;
import ija.xls.notation.GameMove;
import ija.xls.notation.NotationReaderFactory;
import ija.xls.notation.types.GameOperation;
import ija.xls.xml.game.FigureDescription;
import ija.xls.xml.game.FigureDescription.FigureType;
import ija.xls.xml.game.XmlGameReaderWriter;

import java.io.IOException;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.List;

import org.dom4j.DocumentException;

/**
 * Hlavní model pro hru dáma. Reprezentuje datový model MVC architektury
 * využívaný pro běh hry. Implementuje návrhový vzor Observer, pomocí kterého
 * informuje View či Net o svých změnách
 * 
 * @author Radek Necas, xnecas18
 */
public class CheckersModel extends java.util.Observable implements GameModel {
    public static final int PLAYERS_COUNT = 2;

    protected Desk desk;
    protected String message;

    protected History history;
    protected GameMove moveForHistory;
    protected int movesCount;

    protected Player[] players;
    protected int activePlayer;

    XmlGameReaderWriter xmlReaderWriter;

    /**
     * Konstruktor vytváří model.
     */
    public CheckersModel() {
        this.desk = new Desk();

        this.history = new History();
        this.moveForHistory = null;
        this.movesCount = 0;

        this.activePlayer = 0;
        this.players = new Player[PLAYERS_COUNT];

        this.xmlReaderWriter = new XmlGameReaderWriter();
    }

    public String getMessage() {
        return this.message;
    }

    public void setMessage(String message) {
        this.message = message;
        this.setChanged();
        this.notifyObservers(this.message);
    }

    /**
     * Inicializování hrací desky. Metoda naplní šachovnici figurkami do
     * základního postavení.
     * 
     * @param topPlayerColor
     *            udává jakou barvu budou mít figurky v horní části šachovnice
     *            (řádky: 8, 7, 6).
     */
    @Override
    public void initGame(FigureColor topPlayerColor) {
        this.desk.clear();
        int dim = this.desk.getDimension();
        int halfDim = dim / 2;

        FigureColor topColor = topPlayerColor;
        FigureColor downColor = (topPlayerColor == FigureColor.BLACK ? FigureColor.WHITE
                : FigureColor.BLACK);

        Position p;

        for (int row = 0; row < dim; row++) {
            // Vyplnuji se pouze tri radky na zacatku a na konci pole
            if (row >= 3 && row < dim - 3) {
                continue;
            }

            for (int col = 0; col < dim; col++) {
                if ((row + col) % 2 == 0) {
                    p = this.getDesk().getPositionAt(Desk.unmapColumn(col), Desk.unmapRow(row));
                    // Vyber adekvatni figurky (nahore cerna, dole bila)
                    if (row > halfDim) {
                        // p.putFigure(new PieceGoBack(p, topColor));
                        p.putFigure(new Piece(p, topColor, FigureDirection.BACKWARD));
                        // System.out.println("putting top");
                    } else {
                        // p.putFigure(new PieceGoForward(p, downColor));
                        p.putFigure(new Piece(p, downColor, FigureDirection.FORWARD));
                        // System.out.println("putting down");
                    }
                }
            } // end for(ALL_ROWS)
        } // end for(ALL_COLUMNS)
        this.setChanged();
        this.notifyObservers(this.getDesk());
    } // end initGame()

    /**
     * Inicializuje xml vstup.
     * 
     * @param filename
     *            nazev xml souboru
     * @return true ok, false chyba
     */
    @Override
    public boolean initInputXml(String filename) {
        try {
            this.xmlReaderWriter.initInput(filename);
        } catch (DocumentException e) {
            System.err.println("Chyba pri otevirani soboru. " + e.getMessage());
            return false;
        }

        return true;
    }

    /**
     * Naplni sachovnici figurkami dane barvy z xml.
     * 
     * @param color
     *            barva figurky
     * @return true ok, false chyba
     */
    @Override
    public boolean initFigureXml(FigureColor color) {
        if (this.xmlReaderWriter == null) {
            return false;
        }

        Iterator<FigureDescription> it = this.xmlReaderWriter.figuresIterator(color);
        FigureDescription fd;
        Position p;
        while (it.hasNext()) {
            fd = it.next();
            p = this.desk.getPositionAt(fd.getCoordinate());
            if (fd.getFigureType() == FigureType.PIECE) {
                p.putFigure(new Piece(p, fd.getColor(), fd.getDirection()));
            } else if (fd.getFigureType() == FigureType.QUEEN) {
                p.putFigure(new Queen(p, fd.getColor()));
            }
        }

        return true;
    }

    /**
     * Inicializuje hru z xml. Naplni sachovnici figurkami vsech barev.
     * 
     * @return true OK, false chyba
     */
    @Override
    public boolean initGameXml() {
        boolean b = false;
        this.desk.clear();
        if (this.initFigureXml(FigureColor.BLACK)) {
            b = this.initFigureXml(FigureColor.WHITE);
            this.setChanged();
            this.notifyObservers(this.desk);
        }

        return b;
    }

    /**
     * Inicializuje hrace z xml
     * 
     * @return true OK, false chyba
     */
    @Override
    public boolean initPlayersXml() {
        if (this.xmlReaderWriter == null) {
            return false;
        }

        Iterator<Player> it = this.xmlReaderWriter.playersIterator();
        Player p;
        int i = 0;
        while (it.hasNext()) {
            p = it.next();
            if (!this.setPlayer(i, p)) {
                this.clearPlayers();
                return false;
            }
            i++;
        }
        this.setChanged();
        this.notifyObservers(this.players);

        return this.activatePlayer(this.xmlReaderWriter.getActivePlayerId());
    }

    /**
     * Inicializuje stav historie z xml.
     * 
     * @return true OK, false chyba.
     */
    @Override
    public boolean initHistoryXml() {
        if (this.xmlReaderWriter == null) {
            return false;
        }

        Iterator<HistoryMove> it = this.xmlReaderWriter.historyMovesIterator();
        HistoryMove hm;
        while (it.hasNext()) {
            hm = it.next();
            if (hm.isComplete()) {
                this.history.addMove(hm);
            } else {
                if (!it.hasNext()) {
                    this.moveForHistory = hm.getFirstMove();
                    this.setChanged();
                    this.notifyObservers(this.moveForHistory);
                } else {
                    return false;
                }
            }
        }
        this.setChanged();
        this.notifyObservers(this.history);
        return true;
    }

    /**
     * Inicializuje stav modelu z xml objektu. Predpoklada, ze objekt jiz bude
     * vytvoren a naplnen.
     * 
     * @return true OK, false chyba
     */
    @Override
    public boolean initFromXml() {
        this.desk.clear();
        if (this.initGameXml()) {
            if (this.initPlayersXml()) {
                return this.initHistoryXml();
            }
        }

        return false;
    }

    /**
     * Inicializuje stav modelu z xml souboru.
     * 
     * @param filename
     *            cesta k xml souboru
     * @return indikator uspechu operace. True OK, false chyba
     */
    @Override
    public boolean initFromXml(String filename) {
        if (!this.initInputXml(filename)) {
            System.out.println("Load: Fail 2");
            return false;
        }

        return this.initFromXml();
    }

    /**
     * Metoda pro nastavení hráče
     * 
     * @param id
     *            identifikator hráče, jenž se bude nastavovat (povolené 0 a 1).
     * @param p
     *            hráč který se nastaví (zkopíruje se odkaz)
     * @return true pokud je hráč nastaven, false při chybě (špatné id)
     */
    @Override
    public boolean setPlayer(int id, Player p) {
        if (id >= 0 && id < this.players.length) {
            this.players[id] = p;
            p.checkColorUniqueness(this.players);
            p.checkDirectionUniqueness(this.players);
            this.setChanged();
            this.notifyObservers(this.players);
            return true;
        }
        return false;
    }

    /**
     * Metoda nastavi desku dle jine desky. Vsechny aktualni figurky budou
     * straceny a budou nastaveny figurky nove. Rozmery desky se take nastavuji
     * dle nove desky. Metoda informuje observery.
     * 
     * @param otherDesk
     *            deska, podle ktere bude deska modelu nastavena.
     */
    @Override
    public void setDesk(Desk otherDesk) {
        this.desk = otherDesk;
        System.out.println("Model nastavil desku:");
        this.desk.printOccupy();
        this.setChanged();
        this.notifyObservers(this.desk);
    }

    /**
     * Metoda pro získání hráče.
     * 
     * @param id
     *            identifikátor hráče (povolené hodnoty 0 až PLAYERS_COUNT-1).
     * @return hráč nebo null při chybě (špatné id)
     */
    @Override
    public Player getPlayer(int id) {
        if (id >= 0 && id < PLAYERS_COUNT) {
            return this.players[id];
        }

        return null;
    }

    public Player getTopPlayer() {
        for (Player p : this.players) {
            if (p.getDirection() == FigureDirection.BACKWARD) {
                return p;
            }
        }
        return null;
    }

    /**
     * Vrátí aktivního hráče
     * 
     * @return aktivní hráč
     */
    @Override
    public Player getActivePlayer() {
        return this.getPlayer(this.activePlayer);
    }

    /**
     * Vrati neaktivniho hrace
     * 
     * @return druhy tedy neaktivni hrac
     */
    public Player getNoneActivePlayer() {
        if (this.activePlayer == 0) {
            return this.getPlayer(1);
        } else {
            return this.getPlayer(0);
        }
    }

    /**
     * Aktivuje určeného hráče. Pokud není metoda volána, je automaticky aktivní
     * hráč 0. Informuje Observer.
     * 
     * @param id
     *            identifikátor hráče, jenž má být aktivován
     * @return true ok, jinak false (špatné id)
     */
    @Override
    public boolean activatePlayer(int id) {
        if (id >= 0 && id < PLAYERS_COUNT) {
            if (this.players[id] != null) {
                this.activePlayer = id;
                this.setChanged();
                this.notifyObservers(this.players[id]);
                return true;
            }
        }

        return false;
    }

    /**
     * Aktivuje určeného hráče. Pokud není metoda volána, je automaticky aktivní
     * hráč 0. Informuje observer.
     * 
     * @param c
     *            barva hráče, jenž má být aktivován
     * @return true ok, jinak false (špatné id)
     */
    @Override
    public boolean activatePlayer(FigureColor c) {
        for (int i = 0; i < PLAYERS_COUNT; i++) {
            if (this.players[i].getColor() == c) {
                this.activatePlayer(i);
                return true;
            }
        }
        return false;
    }

    /**
     * Aktivuje dalšího hráče (toho, který je na řadě).
     * 
     * @return
     */
    @Override
    public Player nextPlayer() {
        if (this.activePlayer < PLAYERS_COUNT - 1) {
            this.activePlayer++;
        } else {
            this.activePlayer = 0;
        }
        this.setChanged();
        this.notifyObservers(this.getActivePlayer());
        return this.getActivePlayer();
    }

    /**
     * Zrusi vsechny hrace a nastavi aktivniho hrace na 0 (vychozi hodnota).
     */
    @Override
    public void clearPlayers() {
        for (int i = 0; i < PLAYERS_COUNT; i++) {
            this.players[i] = null;
        }
        this.activePlayer = 0;
    }

    /**
     * Metoda pro získání šachovnice. Skrze šachovnici se poté získávají další
     * informace (pozice, figurky). Získané objekty slouží jen pro čtení. Neměli
     * by se přímo měnit.
     * 
     * @return šachovnice, v níž jsou umístěny figurky
     */
    @Override
    public Desk getDesk() {
        return this.desk;
    }

    /**
     * Metoda vycisti desku. Zbavi ji vsech figurek.
     */
    @Override
    public void clearDesk() {
        this.desk.clear();
    }

    /**
     * Vrati historii hry
     * 
     * @return historie hry
     */
    @Override
    public History getHistory() {
        return this.history;
    }

    /**
     * Provádí pohyb figurky. Žádaná operace (skok, pohyb) se zjisti z parametru
     * moveDescription Aktualizuje všechny související objekty (figurky, pozice
     * v dané šachovnici). Pozice musí být na téže šachovnici (ale není
     * testováno). O změně informuje své posluchače (Observer).
     * 
     * @param moveDescription
     *            popisuje pohyb (počáteční, koncová pozice, operace)
     * @return true pokud dojde ke změně modelu, jinak false
     */
    @Override
    public boolean move(GameMove moveDescription) {
        if (moveDescription == null) {
            return false;
        }

        boolean ret = false;
        GameOperation op = moveDescription.getOperation();

        if ((op == GameOperation.MOVE) || (op == GameOperation.MOVESET)) {
            Position startPos = this.desk.getPositionAt(moveDescription.getFirstPosition()
                    .getColumn(), moveDescription.getFirstPosition().getRow());
            Position endPos = this.desk.getPositionAt(
                    moveDescription.getLastPosition().getColumn(), moveDescription
                            .getLastPosition().getRow());
            ret = this.move(startPos, endPos);
            if (ret && this.setQueen(this.desk.getPositionAt(moveDescription.getLastPosition()))) {
                moveDescription.setOperation(GameOperation.MOVESET);
            }
        }

        else if ((op == GameOperation.JUMP) || (op == GameOperation.JUMPSET)) {
            ret = this.jump(moveDescription);
            if (ret && this.setQueen(this.desk.getPositionAt(moveDescription.getLastPosition()))) {
                moveDescription.setOperation(GameOperation.JUMPSET);
            }
        }

        if (ret) {
            this.addMoveToHistory(moveDescription);
            this.setChanged();
            this.notifyObservers(this.desk);
        }

        return ret;
    }

    /**
     * Provádí pohyb figurky. Aktualizuje všechny související objekty (figurky,
     * pozice v dané šachovnici). Pozice musí být na téže šachovnici (ale není
     * testováno).
     * 
     * @param startPosition
     *            počáteční pozice. Zde se nachází pohybovaná figurka.
     * @param endPosition
     *            cílová pozice pohybu.
     * @return pokud je pohyb proveden true, jinak false.
     */
    public boolean move(Position startPosition, Position endPosition) {
        if (!this.desk.isInside(startPosition) || !this.desk.isInside(endPosition)
                || startPosition.getFigure() == null) {
            return false;
        }

        // Bezny pohyb figurkou
        boolean moveOk = startPosition.getFigure().move(endPosition);
        /*
         * if (moveOk) { this.setChanged(); this.notifyObservers(this.desk); }
         */

        return moveOk;
    }

    /**
     * Metoda provadi skok dle popisu
     * 
     * @param moveDescription
     *            popis tahu - skoku
     * @return true skok proveden, jinak false (chyba)
     */
    public boolean jump(GameMove moveDescription) {
        if ((moveDescription.getOperation() != GameOperation.JUMP)
                && moveDescription.getOperation() != GameOperation.JUMPSET) {
            return false;
        }

        Position startPos;
        Position endPos = null;
        Iterator<DeskCoordinate> it = moveDescription.iterator();
        if (it.hasNext()) {
            startPos = this.desk.getPositionAt(it.next());
        } else {
            return false;
        }
        while (it.hasNext()) {
            endPos = this.desk.getPositionAt(it.next());
            if (!this.jump(startPos, endPos)) {
                return false;
            }
            startPos = endPos;
        }
        /*
         * this.setQueen(endPos); this.setChanged();
         * this.notifyObservers(this.desk);
         */
        return true;
    }

    /**
     * Provádí skok figurky. Mezi počáteční a cílovou pozicí musí být figurka
     * protihráče. Provádí pouze jednoduchý skok. Víceskoky se reprezentují
     * vícenásobným voláním metody. Aktualizuje všechny související objekty
     * (počáteční pozici a figurku na ní, přeskakovanou figurku a její pohyb,
     * cílovou pozici).
     * 
     * @param startPosition
     *            počáteční pozice skoku. Zde se nachází figurka s níž chceme
     *            skok provést.
     * @param endPosition
     *            cílová pozice skoku.
     * @return pokud je skok proveden true, jinak false.
     */
    public boolean jump(Position startPosition, Position endPosition) {
        if (!this.desk.isInside(startPosition) || !this.desk.isInside(endPosition)
                || startPosition.getFigure() == null) {
            return false;
        }

        CheckersFigure f = (CheckersFigure) startPosition.getFigure();
        boolean jumpOk = f.jump(endPosition);

        return jumpOk;
    }

    /**
     * Test zda může být na dané pozici nasazena královna.
     * 
     * @param p
     *            testovaná pozice
     * @return pokud test projde true, jinak false.
     */
    public boolean canSetQueen(Position p) {
        if ((p == null) || !this.desk.isInside(p) || p.getFigure() == null) {
            return false;
        }

        Figure f = p.getFigure();
        if (f instanceof Piece) {
            Piece piece = (Piece) f;
            if (piece.getDirection() == FigureDirection.FORWARD
                    && p.getRowNumber() == this.desk.getDimension()) {
                return true;
            } else if (piece.getDirection() == FigureDirection.BACKWARD && p.getRowNumber() == 1) {
                return true;
            }
        }

        return false;
    }

    /**
     * Metoda pro nasazení královny. Pouze provádí záměnu figurek (pěšce za
     * královnu). Neprovádí posuv na nasazovací pozici (už očekává, že tam
     * figurka je).
     * 
     * @param p
     *            pozice, kde se má nasadit dáma.
     * @return při nasazení true, jinak false.
     */
    public boolean setQueen(Position p) {
        if (this.canSetQueen(p)) {
            Figure f = p.removeFigure();
            p.putFigure(new Queen(p, f.getColor()));

            return true;
        }

        return false;
    }

    /**
     * Metoda premeni damu zpet na bezneho pesce.
     * 
     * @param p
     *            pozice na ktere musi byt nasazena dama
     * @return nove nasazena figurka
     */
    public Figure unsetQueen(Position p) {
        Figure f = p.getFigure();
        if (f instanceof Queen) {
            FigureDirection d;
            if (f.getPosition().getRowNumber() == 1) {
                d = FigureDirection.BACKWARD;
            } else if (f.getPosition().getRowNumber() == this.desk.getDimension()) {
                d = FigureDirection.FORWARD;
            } else {
                return null;
            }
            // p.removeFigure();
            Figure fRet = new Piece(p, f.getColor(), d);
            p.putFigure(fRet);
            return fRet;
        }

        return null;
    }

    /**
     * Prida pohyb do lokalni historie modelu. Pohyb historie se sklada ze dvou
     * tahu: bily, cerny + id
     * 
     * @param gm
     *            pohyb k ulozeni
     */
    private void addMoveToHistory(GameMove gm) {
        // Pokud jiz mam ukladat
        if (this.moveForHistory == null) {
            this.moveForHistory = gm;
            this.setChanged();
            this.notifyObservers(this.moveForHistory);
        } else {
            this.history.addMove(new HistoryMove(++this.movesCount, this.moveForHistory, gm));
            this.moveForHistory = null;
        }
        this.setChanged();
        this.notifyObservers(this.history);
    }

    /**
     * Inicializuje xml vystup
     * 
     * @param notType
     *            typ notace, ktera se pouziva ve hre a ve ktere bude ulozena
     *            historie v xml
     */
    @Override
    public void initOutputXml(NotationReaderFactory.NotationType notType) {
        this.xmlReaderWriter.initOutput(notType);
    }

    /**
     * Ulozi vsechny hrace do xml.
     */
    @Override
    public void savePlayersXml() {
        for (int i = 0; i < this.players.length; i++) {
            this.xmlReaderWriter.addPlayerElement(i, this.players[i]);
        }
    }

    /**
     * Ulozi do xml celou historii.
     */
    @Override
    public void saveHistoryXml() {
        int i = 0;
        for (HistoryMove hm : this.history) {
            this.xmlReaderWriter.addHistoryMoveElement(hm);
            i++;
        }

        if (this.moveForHistory != null) {
            HistoryMove hm = new HistoryMove(i, this.moveForHistory, new GameMove());
            this.xmlReaderWriter.addHistoryMoveElement(hm);
        }
    }

    /**
     * Ulozi do xml aktualni stav desky - vsechny figurky na desce. Slouzi pri
     * ulozeni rozehrane hry. Tento stav se pote muze obnovit.
     */
    @Override
    public void saveDeskStateXml() {
        List<Figure> flist = this.desk.getAllFigures();

        for (Figure f : flist) {
            this.xmlReaderWriter.addFigureElement(f);
        }

        this.xmlReaderWriter.addActivePlayerElement(this.activePlayer);
    }

    /**
     * Ulozi do xml cely model. Uklada hrace, historii a stav desky
     */
    @Override
    public void saveAllXml() {
        this.savePlayersXml();
        this.saveHistoryXml();
        this.saveDeskStateXml();
    }

    /**
     * Zapise xml do souboru. Predpoklada, ze xml objekty budou naplneny daty,
     * jinak vytvori jen dokument s korenovym elementem.
     * 
     * @param filename
     *            nazev souboru kam se bude xml zapisovat
     * @throws IOException
     *             chyba pri praci se soubory
     */
    @Override
    public void writeXml(String filename) throws IOException {
        this.xmlReaderWriter.writeToFile(filename);
    }

    /**
     * Zapise xml do PrintStreamu. Predpoklada, ze xml objekty budou naplneny
     * daty, jinak vytvori jen dokument s korenovym elementem.
     * 
     * @param output
     *            PrintStream do ktereho se bude zapisovat.
     * @throws IOException
     *             chyba pri praci se soubory
     */
    @Override
    public void writeXml(PrintStream output) throws IOException {
        this.xmlReaderWriter.writeToStream(output);
    }

    /**
     * Ulozi cely stav modelu do xml a to pote zapise do souboru. Uklada hrace,
     * historii a stav desky.
     * 
     * @param filename
     *            nazev souboru, kam bude xml ulozeno.
     * @throws IOException
     *             chyba pri praci se soubory
     */
    @Override
    public void saveAllXmlAndWrite(String filename) throws IOException {
        this.saveAllXml();
        this.writeXml(filename);
    }

    /**
     * Ulozi cely stav modelu do xml a to pote zapise do PrintStreamu. Uklada
     * hrace, historii a stav desky.
     * 
     * @param output
     *            PrintStream do ktereho se bude zapisovat.
     * @throws IOException
     *             chyba pri praci se streamy.
     */
    @Override
    public void saveAllXmlAndWrite(PrintStream output) throws IOException {
        this.saveAllXml();
        this.writeXml(output);
    }
}
